import java.net.Socket;
import java.util.*;
import java.util.regex.*;


/**
 * This class encompasses the functionality of a Schwimmen game. Has all the logic for "card" actions to take,
 * hand comparisons, etc.
 * @author Chris Fraser, Jean-Luc Richard
 */
public class SchwimmenGame
{
	
	/**
	 * Regex capture for a card
	 */
	public static final String CARD_CAPTURE = "([A-Z0-10]\\w)";
	
	/**
	 * The SchwimmenActions class contains all required actions which can
	 * be ran by the user.
	 */
	public SchwimmenActions actions;
	
	/**
	 * Regex pattern for acknowledgement
	 */
	private Pattern ack = Pattern.compile("ack");
	
	/**
	 * Regex pattern for pass
	 */
	private Pattern pass = Pattern.compile("pass");
	
	/**
	 * Regex pattern for a group of cards
	 */
	private Pattern exchangeCard = Pattern.compile(CARD_CAPTURE + "," + CARD_CAPTURE);
	
	/**
	 * Regex pattern for feuer with a card exchange
	 */
	private Pattern feuerExchange = Pattern.compile(CARD_CAPTURE + "," + CARD_CAPTURE + "," + "feuer");
	
	/**
	 * Regex pattern for schnauz with a card exchange
	 */
	private Pattern schnauzExchange = Pattern.compile(CARD_CAPTURE + "," + CARD_CAPTURE + "," + "schnauz");
	
	/**
	 * Regex pattern for card exchange with a game ending 'close'
	 */
	private Pattern close = Pattern.compile(CARD_CAPTURE + "," + CARD_CAPTURE + "," + "close");
	
	/**
	 * Regex pattern for a dealt feuer
	 */
	private Pattern feuer = Pattern.compile("feuer");
	
	/**
	 * Regex pattern for a dealt schnauz
	 */
	private Pattern schnauz = Pattern.compile("schnauz");
	
	/**
	 * Regex pattern for a returning player.
	 */
	private Pattern returningPlayer = Pattern.compile("R, \\w+");
	
	/**
	 * Regex pattern for a new player.
	 */
	private Pattern newPlayer = Pattern.compile("N, \\w+");
	
	/**
	 * The number of players in the game
	 */
	public int numPlayers;
	
	/**
	 * A collection array of all hands
	 */
	private String[][] hands;
	
	/**
	 * The board is all cards available to each player for swapping.
	 */
	private String[] board;
	
	/**
	 * Count for the number of passes done consecutively
	 */
	private int lastMovePassCount = 0;
	
	/**
	 * Token counter
	 */
	private int[] tokenCounter; 
	
	/**
	 * Boolean value to make sure we know someone called end game.
	 */
	private boolean closed = false;
	
	/**
	 * Storage of the player history
	 */
	private PlayerHistory ph;
	
	/**
	 * Constructor for the SchwimmenGame class
	 * @param The number of players
	 */
	public SchwimmenGame(int numPlayers)
	{
		this.numPlayers = numPlayers;
		this.actions = new SchwimmenActions();
		this.hands = new String[this.numPlayers][3];
		this.board = new String[3];
		this.tokenCounter = new int[this.numPlayers];
		this.ph = new PlayerHistory();
	}
	
	/**
	 * Takes in the String from the client and determines actions to take.
	 * @param protIn The String from the client
	 * @param gm The GameManager that initialized this game
	 * @param currentPlayer The number of the current player
	 * @return String The String to pass back to the client
	 */
	public String newAction(GameManager gm, String protIn, int currentPlayer)
	{
		/*
		 * Regular expression checking of what's going on needs to be done here
		 */
		while (true)
		{
			//Check for a new player
			if(newPlayer.matcher(protIn).matches())
			{
				gm.writeToSocket(this.ph.addPlayer(protIn), currentPlayer, currentPlayer);
				break;
			}
			
			//Check for returning player
			if(returningPlayer.matcher(protIn).matches())
			{
				gm.writeToSocket(this.ph.checkPlayer(protIn), currentPlayer, currentPlayer);
				break;
			}
			
			//If this is an exchange card request
			if (exchangeCard.matcher(protIn).matches())
			{
				if (this.closed)
				{
					this.exchangeCards(protIn.split(",")[0], protIn.split(",")[1], currentPlayer);
					gm.writeToSocket("ack", currentPlayer, currentPlayer);
					this.gameComplete(gm, this.checkWinner());
				}
				else
				{
					this.exchangeCards(protIn.split(",")[0], protIn.split(",")[1], currentPlayer);
					gm.writeToSocket("ack", currentPlayer, currentPlayer);
					
					int nextPlayer = 1;
					if (currentPlayer == this.numPlayers)
					{
						nextPlayer = 1;
					}	
					else
					{
						nextPlayer = currentPlayer++;
					}
					gm.writeToSocket(protIn, currentPlayer, nextPlayer);
				}
				break;
			}
			
			if (pass.matcher(protIn).matches())
			{
				if (this.lastMovePassCount == (this.numPlayers - 1))
				{	
					this.actions.updateTable(this.board, this.hands, this.numPlayers);
					int nextPlayer = currentPlayer + 1;
					if (currentPlayer == 4)
						nextPlayer = 1;
					this.lastMovePassCount ++;
					gm.writeToSocket(this.boardToString(), currentPlayer, nextPlayer);
				}
				else
				{
					int nextPlayer = currentPlayer + 1;
					if (currentPlayer == 4)
						nextPlayer = 1;
					this.lastMovePassCount ++;
					gm.writeToSocket("ack", currentPlayer, nextPlayer);
					gm.writeToSocket("pass", nextPlayer, nextPlayer);
				}
			}
			
			if (ack.matcher(protIn).matches())
			{
				if (currentPlayer == this.numPlayers)
				{
					for(int i = 0; i < this.numPlayers - 1; i++)
					{
						gm.writeToSocket(this.boardToString(), i, i+1);
					}
				}
				else
				{
					return this.boardToString();
				}
				break;
			}
			
			if (feuerExchange.matcher(protIn).matches())
			{
				this.exchangeCards(protIn.split(",")[0], protIn.split(",")[1], currentPlayer);
				if(this.checkFeuer(currentPlayer))
				{
					gm.writeToSocket("ack", currentPlayer, currentPlayer);
					int nextPlayer = 1;
					if (currentPlayer == this.numPlayers)
					{
						nextPlayer = 1;
					}	
					else
					{
						nextPlayer = currentPlayer++;
					}
					gm.writeToSocket(protIn, currentPlayer, nextPlayer);
					this.gameComplete(gm, currentPlayer);
				}
				break;
			}
			
			if (schnauzExchange.matcher(protIn).matches())
			{
				this.exchangeCards(protIn.split(",")[0], protIn.split(",")[1], currentPlayer);
				if(this.checkSchnauz(currentPlayer))
				{
					gm.writeToSocket("ack", currentPlayer, currentPlayer);
					int nextPlayer = 1;
					if (currentPlayer == this.numPlayers)
					{
						nextPlayer = 1;
					}	
					else
					{
						nextPlayer = currentPlayer++;
					}
					gm.writeToSocket(protIn, currentPlayer, nextPlayer);
					this.gameComplete(gm, currentPlayer);
				}
				break;
			}
			
			if (schnauz.matcher(protIn).matches())
			{
				this.exchangeCards(protIn.split(",")[0], protIn.split(",")[1], currentPlayer);
				if(this.checkSchnauz(currentPlayer))
				{
					gm.writeToSocket("ack", currentPlayer, currentPlayer);
					int nextPlayer = 1;
					if (currentPlayer == this.numPlayers)
					{
						nextPlayer = 1;
					}	
					else
					{
						nextPlayer = currentPlayer++;
					}
					gm.writeToSocket(protIn, currentPlayer, nextPlayer);
					this.gameComplete(gm, currentPlayer);
				}
				break;
			}
			
			if (feuer.matcher(protIn).matches())
			{
				this.exchangeCards(protIn.split(",")[0], protIn.split(",")[1], currentPlayer);
				if(this.checkFeuer(currentPlayer))
				{
					gm.writeToSocket("ack", currentPlayer, currentPlayer);
					int nextPlayer = 1;
					if (currentPlayer == this.numPlayers)
					{
						nextPlayer = 1;
					}	
					else
					{
						nextPlayer = currentPlayer++;
					}
					gm.writeToSocket(protIn, currentPlayer, nextPlayer);
					this.gameComplete(gm, currentPlayer);
				}
				break;
			}
			
			if (close.matcher(protIn).matches())
			{
				this.exchangeCards(protIn.split(",")[0], protIn.split(",")[1], currentPlayer);
				gm.writeToSocket("ack", currentPlayer, currentPlayer);
				int nextPlayer = 1;
				if (currentPlayer == this.numPlayers)
				{
					nextPlayer = 1;
				}	
				else
				{
					nextPlayer = currentPlayer++;
				}
				gm.writeToSocket(protIn, currentPlayer, nextPlayer);
				this.closed = true;
				break;
			}			
		}
		return null;
	}
	
	/**
	 * This method initializes game by handing out hand and table cards
	 * @param GameManager object used for writing out to socket
	 */
	public void startGame(GameManager gm)
	{
		Logger.info("Starting the game.");
		for (int j=0; j < this.numPlayers; j++)
		{
			String[] newHand = actions.newHand();
			for (int i=0; i < 3; i++)
			{
				this.hands[j][i] = newHand[i];
			}
		}
		
		Logger.info("Filling up the board.");
		String[] newBoard = actions.newHand();
		for (int i=0; i < 3; i++)
		{
			this.board[i] = newBoard[i];
		}
		
		Logger.info("Writing to player 1's socket");
		gm.writeToSocket("1," + this.hands[0][0] + "," + this.hands[0][1] + ","
				+ this.hands[0][2], 1, 1);
	}
   
   /**
    * This method exchanges two cards: one from a player's hand and another from the table.
    * @param newCard The card from the table that the player wishes to exchange
    * @param oldCard The card in the player's hand that the player wishes to exchange
    * @param board the String[] of the cards on the table
    * @param currentPlayer the currentPlayer number
    * @return true if exchange is valid and successful, false otherwise
    */
    public boolean exchangeCards(String newCard, String oldCard, int currentPlayer)
    {
      //Cycle through hand, if newCard is in the current player's hand, continue.
      //Else: return false
          
    	for(int i=0; i < this.hands[currentPlayer-1].length; i++)
    	{
	        //Found card
	        if(oldCard.compareTo(this.hands[currentPlayer-1][i]) == 0)
	        {
	        	//Check for the card on the table
	        	for(int j=0; j < this.board.length; j++)
		        {
		        //Card on table is there
			        if(newCard.compareTo(this.board[j]) == 0)
			        {
			        	//Swap
			            String temp;
			            temp = this.board[j];
			            this.board[j] = this.hands[currentPlayer-1][i];
			            this.hands[currentPlayer-1][i] = temp;
			                  
			            return true;
			        }
		        }
	        }
    	}
      
    	return false;
    } 
    
    /**
     * This method returns a String representation for a the table
     * (e.g. table has "AC, 7H, JD", return "AC,7H,JD").
     * @return String Board's "String" form
     */
    private String boardToString()
    {
    	StringBuilder sb = new StringBuilder("");
    	for(int i = 0; i < this.board.length; i++)
    	{
    		sb.append(this.board[i]);
    		if ((i+1) != this.board.length)
    		{
    			sb.append(",");
    		}
    	}
    	return sb.toString();
    }
    
    /**
     * This method returns a String representation for a player's hand
     * (e.g. player has "AC, 7H, JD", return "AC,7H,JD").
     * @param playerNum The player number to call this method upon
     * @return String the player's hand's "String" form
     */
    private String handToString(int playerNum)
    {
    	StringBuilder sb = new StringBuilder("");
    	for(int i = 0; i < 3; i++)
    	{
    		sb.append(this.hands[playerNum - 1][i]);
    		if ((i+1) != this.board.length)
    		{
    			sb.append(",");
    		}
    	}
    	return sb.toString();
    }
    
    /**
     * This method checks if player number given has a feuer combination.
     * Returns true if it is a feuer combination, false otherwise.
     * @param currentPlayer The player number to check
     * @return true if player has a Feuer combination, false otherwise
     */
    private boolean checkFeuer(int currentPlayer)
    {
    	String card1 = handToString(currentPlayer).split(",")[0];
    	String card2 = handToString(currentPlayer).split(",")[1];
    	String card3 = handToString(currentPlayer).split(",")[2];
    	
    	if (!(card1.substring(card1.length() - 1).equalsIgnoreCase(card2.substring(card2.length() - 1)) &&
    			card1.substring(card1.length() - 1).equalsIgnoreCase(card3.substring(card3.length() - 1))))
    	{
    		return false;
    	}
    	
    	if(!(this.getPoint(card1) + this.getPoint(card2) + this.getPoint(card3) == 31))
    	{
    		return false;
    	}
    	return true;
    }
    
    /**
     * This method checks if player number given has a schnauz combination.
     * Returns true if it is a schnauz combination, false otherwise.
     * @param currentPlayer The player number to check
     * @return true if player has a Schnauz combination, false otherwise
     */
    private boolean checkSchnauz(int currentPlayer)
    {
    	String card1 = handToString(currentPlayer).split(",")[0];
    	String card2 = handToString(currentPlayer).split(",")[1];
    	String card3 = handToString(currentPlayer).split(",")[2];
    	
    	if (!(card1.substring(0, card1.length() - 1).equalsIgnoreCase(card2.substring(0, card2.length() - 1)) &&
    			card1.substring(0, card1.length() - 1).equalsIgnoreCase(card3.substring(0, card3.length() - 1))))
    	{
    		return false;
    	}
    	
    	return true;
    }
    
    /**
     * This method compares all players' hands and 
     * returns player number that wins (**or 0 if a tie**)
     * @return int player number that is the winner (0 is a tie), (-1 if error)
     */
    private int checkWinner()
    {
    	//call calculateHand on all players
    	double currentPoints = 0;
    	int winningPlayer = -1;
    	double nextPoints = 0;
    	String currentSuit;
    	String nextSuit;
    	String currentNumber;
    	String nextNumber;
    	
    	currentPoints = calculateHand(this.getHighestCards(1), 1);
    	
    	for(int i=1; i < hands.length; i++)
    	{
    		nextPoints = calculateHand(this.getHighestCards(i+1), i+1);
    		
    		//If next player has more points than current player,
    		//next player is now the winning player
    		if(nextPoints > currentPoints)
    		{
    			winningPlayer = i + 1;
    		}
    		if(nextPoints == currentPoints)
    		{
    			//If both schnauz, compare card numbers:
    			if(this.checkSchnauz(i) && this.checkSchnauz(i+1))
    			{
    				currentNumber = this.getHighestCards(i)[0].substring(0, this.getHighestCards(i)[0].length() - 1);
        			nextNumber = this.getHighestCards(i+1)[0].substring(0, this.getHighestCards(i+1)[0].length() - 1);
        			
        			//If point value of card number of schnauz is different
        			if(this.getPoint(getHighestCards(i)[0]) > this.getPoint(getHighestCards(i+1)[0]))
        			{
        				winningPlayer = i;
        			}
        			if(this.getPoint(getHighestCards(i)[0]) < this.getPoint(getHighestCards(i+1)[0]))
        			{
        				winningPlayer = i+1;
        			}
        			//If point value of card number of schnauz is same, check which is "highest"
        			if(this.getPoint(getHighestCards(i)[0]) == this.getPoint(getHighestCards(i+1)[0]))
        			{
        				//If both are same suit, something's wrong
        				if(currentNumber.compareTo(nextNumber) == 0)
        				{
        					Logger.error("Two schnauz hands have same suit.");
        					winningPlayer = -1;
        				}
        				
        				//If current has highest card number, current wins: all cases
        				if(currentNumber.compareTo("K") == 0)
        				{
        					winningPlayer = i;
        				}
        				if(currentNumber.compareTo("Q") == 0 && nextNumber.compareTo("K") != 0)
        				{
        					winningPlayer = i;
        				}
        				if(currentNumber.compareTo("J") == 0 && nextNumber.compareTo("Q") != 0
        						&& nextNumber.compareTo("K") != 0)
        				{
        					winningPlayer = i;
        				}
        				//Else next is winner
        				else
        					winningPlayer = i+1;
        			}
    			}
    			
    			//If not, compare suits
    			currentSuit = this.getHighestCards(i)[0].substring(this.getHighestCards(i)[0].length() - 1);
    			nextSuit = this.getHighestCards(i+1)[0].substring(this.getHighestCards(i+1)[0].length() - 1);
    			
    			//If current is lowest alphabetical suit, then current wins
    			if(currentSuit.compareTo(nextSuit) < 0)
    			{
    				winningPlayer = i;
    			}
    			else if(currentSuit.compareTo(nextSuit) > 0)
    			{
    				winningPlayer = i+1;
    			}
    			//No winners
    			else
    			{
    				winningPlayer = 0;
    			}
    		}
    		else
    			winningPlayer = i;
    	}
    	return winningPlayer;
    }
    
    /**
     * This method sends all protocol messages out to the players at the end of the 
     * game and updates the token counter.
     * @param gm GameManager for this game
     * @param winningPlayerNum player number of the winner
     */
    private void gameComplete(GameManager gm, int winningPlayerNum)
    {
    	for (int h = 0; h < this.numPlayers; h++)
    	{
    		if(h != winningPlayerNum)
    		{
    			this.tokenCounter[h]--;
    		}
    	}
    	for ( int n = 0; n < this.numPlayers; n++)
    	{
    		for (int k = 0; k < this.numPlayers; k++)
    		{
    			if (k == n)
    				continue;
    			gm.writeToSocket(this.handToString(n), k, n);
    		}
    	}
    	for ( int i = 0; i < this.numPlayers; i++)
    	{
    		StringBuilder sb = new StringBuilder("");
    		for ( int j = 0; j < this.numPlayers; j++)
    		{
    			sb.append(this.calculateHand(this.getHighestCards(j+1), j+1));
    			sb.append(",");
    		}
    		if (i+1 == winningPlayerNum)
    		{
    			sb.append("win,");
    		}
    		if (0 == winningPlayerNum)
    		{
    			sb.append("tie,");
    		}
    		else
    		{
    			sb.append("loss,");
    		}
    		for (int k = 0; k < this.numPlayers; k++)
    		{
    			sb.append(this.tokenCounter[k]);
    			if (k != this.numPlayers - 1)
    			{
    				sb.append(",");
    			}
    		}
    		if (sb.toString().contains("win"))
    		{
    			ph.getPlayers()[i].updateRecord(Player.WIN_VALUE);
    		}
    		else if (sb.toString().contains("loss"))
    		{
    			ph.getPlayers()[i].updateRecord(Player.LOSS_VALUE);
    		}
    		gm.writeToSocket(sb.toString(), i, i);
    	}
    	
    	ph.writeData();
    }
    
    /**
     * This method calculates the points worth for a player's hand and returns the value of the hand
     * @param player The number of the player to calculate points
     * @param winningCards String[] of the cards in the player's hand that count towards the hand's score
     * @return double Hand's point value
     */
    private double calculateHand(String[] winningCards, int player)
    {
    	int points = 0;
    	
    	if(this.checkSchnauz(player))
    	{
    		if(this.hands[player - 1][0] == "A" && this.hands[player - 1][1] == "A" && this.hands[player - 1][2] == "A")
    		{
    			return 32;
    		}	
    		else
    			return 30.5;
    	}
    	
    	if(this.checkFeuer(player))
    	{
    		return 31;
    	}
    	else
    	{
    		for(int i=0; i < winningCards.length; i++)
    		{
    			points += this.getPoint(winningCards[i]);
    		}
    		return points;
    	}
    }
    
    
    /**
     * This method returns the point value of a card.
     * @param cardNumber The "number" of a card (e.g. "8" or "K")
     * @return int Points for the card
     */
    private int getPoint(String cardNumber)
    {
    	if(cardNumber.compareTo("7") == 0)
    		return 7;
    	if(cardNumber.compareTo("8") == 0)
    		return 8;
    	if(cardNumber.compareTo("9") == 0)
    		return 9;
    	if(cardNumber.compareTo("K") == 0 || 
    			cardNumber.compareTo("Q") == 0 ||
    			cardNumber.compareTo("J") == 0 ||
    			cardNumber.compareTo("10") == 0)
    	{
    		return 10;
    	}
    	if(cardNumber.compareTo("A") == 0)
    		return 11;
    	//Card number isn't in a schwimmen game
    	else
    		Logger.error("Card number is not 7, 8, 9, 10, J, Q, K, or A.");
    		return 0;
    }
    
    /**
     * This method finds the winning cards in a given player's hand and returns ONLY the cards
     * that make the score of the hand (e.g. all three if schnauz or feuer or three same suit,
     * two cards if two are same suit, one if none are same)
     * @param player The player number to get winning cards from
     * @return String[] The array of cards that make the hand's total score
     */
    private String[] getHighestCards(int player)
    {
    	String[] highestCards;
    	
    	String[] first = new String[2];
    	first[0] = this.hands[player - 1][0].substring(0, this.hands[player - 1][0].length() - 1);
    	first[1] = this.hands[player - 1][0].substring(this.hands[player - 1][0].length() - 1);
    	
    	String[] second = new String[2];
    	second[0] = this.hands[player - 1][1].substring(0, this.hands[player - 1][1].length() - 1);
    	second[1] = this.hands[player - 1][1].substring(this.hands[player - 1][1].length() - 1);
    	
    	String[] third = new String[2];
    	third[0] = this.hands[player - 1][2].substring(0, this.hands[player - 1][2].length() - 1);
    	third[1] = this.hands[player - 1][2].substring(this.hands[player - 1][2].length() - 1);
    	
    	//Check for schnauz and feuer before anything
    	if(this.checkSchnauz(player))
    	{
    		highestCards = this.hands[player - 1];
    		return highestCards;
    	}
    		
    	if(this.checkFeuer(player))
    	{
    		highestCards = this.hands[player - 1];
			return highestCards;
    	}
    	
    	//Compare first and second if they are the same suit
    	if(first[1].compareTo(second[1]) == 0)
    	{
    		//If so, compare first and third if they are the same suit
    		if(first[1].compareTo(third[1]) == 0)
    		{
    			highestCards = this.hands[player - 1];
    			return highestCards;
    		}
    		else
    		{
    			String[] temp = {this.hands[player - 1][0], this.hands[player - 1][1]};
    			highestCards = temp;
    			return highestCards;
    		}
    	}
    	//If not, compare first and third if they are the same suit
    	else
    	{
    		//If so, add 1,3 cards' value
    		if(first[1].compareTo(third[1]) == 0)
    		{
    			String[] temp = {this.hands[player - 1][0], this.hands[player - 1][2]};
    			highestCards = temp;
				return highestCards;
    		}
    		//If not, compare second and third if they are the same suit
    		else
    		{
    			//If so, add 2,3 cards' value
    			if(second[1].compareTo(third[1]) == 0)
    			{
    				String[] temp = {this.hands[player - 1][1], this.hands[player - 1][2]};
        			highestCards = temp;
        			return highestCards;
    			}

    			//If not, find largest valued card of the three
    			else
    			{
    				if(getPoint(first[0]) > getPoint(second[0]))
    				{
    					if(getPoint(first[0]) > getPoint(third[0]))
    					{
    						String[] temp = {this.hands[player - 1][0]};
    						highestCards = temp;
    						return highestCards;
    					}
    					else
    					{
    						String[] temp = {this.hands[player - 1][2]};
    		    			highestCards = temp;
    		    			return highestCards;
    					}
    				}
    				else
    				{
    					if(getPoint(second[0]) > getPoint(third[0]))
    					{
    						String[] temp = {this.hands[player - 1][1]};
    		    			highestCards = temp;
    		    			return highestCards;
    					}
    					else
    					{
    						String[] temp = {this.hands[player - 1][2]};
    		    			highestCards = temp;
    		    			return highestCards;
    					}
    				}
    			}
    		}
    	}
    }
}
